﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using LockCrypt.Core;
using Lime49.UI;
using LockCrypt.Converters;

namespace LockCrypt {
    /// <summary>
    /// Control for editing customizable fields.
    /// </summary>
    public partial class FieldEditor : UserControl {
        private Dictionary<string, List<UIElement>> _controlMap = new Dictionary<string, List<UIElement>>();
        private Window _parent;
        private BitmapImage _imgSettings,
                            _imgKey;
        private Style _textBoxStyle;

        public static readonly DependencyProperty FieldsProperty = DependencyProperty.Register("Fields", typeof(IList<Field>), typeof(FieldEditor), new UIPropertyMetadata(new List<Field>(), new PropertyChangedCallback(Fields_Changed)));
        /// <summary>
        /// Sets the fields to edit.
        /// </summary>
        /// <value>The fields to edit.</value>
        public IList<Field> Fields {
            private get { return (IList<Field>)base.GetValue(FieldsProperty); }
            set { base.SetValue(FieldsProperty, value); }
        }

        /// <summary>
        /// Creates editing controls when the fields change.
        /// </summary>
        /// <param name="d">The FieldEditor.</param>
        /// <param name="e">The <see cref="System.Windows.DependencyPropertyChangedEventArgs"/> instance containing the event data.</param>
        private static void Fields_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e) {
            FieldEditor fe = (FieldEditor)d;
            var newFields = fe.Fields;
            newFields.OrderBy(f => f.SortIndex);
            fe.grdFields.Children.Clear();
            fe._controlMap.Clear();
            foreach(Field field in newFields) {
                fe.AddRow(field);
            }
        }

        /// <summary>
        /// Adds a row of editing controls.
        /// </summary>
        /// <param name="field">The field.</param>
        private void AddRow(Field field) {
            if(_imgSettings == null) {
                _imgSettings = new BitmapImage(new Uri("pack://application:,,,/Resources/Images/settings_16.png", UriKind.RelativeOrAbsolute));
            }
            if(_imgKey == null) {
                _imgKey = new BitmapImage(new Uri("pack://application:,,,/Resources/Images/key_16.png", UriKind.RelativeOrAbsolute));
            }
            if(_textBoxStyle == null) {
                _textBoxStyle = TryFindResource("MetaDataTextBox") as Style;
            }
            grdFields.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(0, GridUnitType.Auto) });
            // the label's DataContext holds the actual field
            Label lbl = new Label() {
                DataContext = field,
            };
            Binding lblBinding = new Binding("DataContext.Name") {
                RelativeSource = new RelativeSource(RelativeSourceMode.Self),
                Mode = BindingMode.TwoWay,
                UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
            };
            lbl.SetBinding(ContentControl.ContentProperty, lblBinding);
            TextBox txt = new TextBox() { 
                DataContext = field,
                Style = _textBoxStyle
            };
            Binding txtBinding = new Binding("DataContext.Value") {
                Source = lbl,
                Mode = BindingMode.TwoWay,
                UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
            };
            txt.SetBinding(TextBox.TextProperty, txtBinding);
            txt.SetBinding(Grid.RowProperty, new Binding("DataContext.SortIndex") {
                Source = lbl,
            });

            // menubutton and menu
            IValueConverter metaConverter=new MetaFlagsToBoolConverter(),
                            metaVisibilityConverter = new MetaFlagsToVisibilityConverter();
                            //isMetaFieldConverter = new IsMetaFieldConverter();
            IMultiValueConverter isMetaFieldConverter = new IsMetaFieldConverter();
            ContextMenu mnu = new ContextMenu();
            mnu.Items.Add(new MenuItem() {
                Header = I18nUtils.GetString("Strings", "RenameField"),
                Command = ApplicationCommands.Properties,
                CommandTarget = lbl
            });
            mnu.Items.Add(new MenuItem() {
                Header = I18nUtils.GetString("Strings", "DeleteField"),
                Command = ApplicationCommands.Delete,
                CommandTarget = lbl
            });
            mnu.Items.Add(new MenuItem() {
                Header = I18nUtils.GetString("Strings", "MoveUp"),
                Command = NavigationCommands.GoToPage,
                CommandTarget = lbl,
                CommandParameter = -1
            });
            mnu.Items.Add(new MenuItem() {
                Header = I18nUtils.GetString("Strings", "MoveDown"),
                Command = NavigationCommands.GoToPage,
                CommandTarget = lbl,
                CommandParameter = 1
            });
            mnu.Items.Add(new Separator());
            MenuItem itmUrl = new MenuItem() {
                Header = I18nUtils.GetString("Strings", "MarkAsUrl"),
                Command = LockCryptCommands.MarkAsUrl,
                CommandTarget = lbl,
                DataContext = field
            };
            itmUrl.SetBinding(MenuItem.IsCheckedProperty, new Binding("DataContext.Meta") { 
                RelativeSource=new RelativeSource(RelativeSourceMode.Self), 
                Converter = metaConverter, 
                ConverterParameter = FieldMetaData.Url, 
            });
            mnu.Items.Add(itmUrl);
            MenuItem itmPassword = new MenuItem() {
                Header = I18nUtils.GetString("Strings", "MarkAsPassword"),
                Command = LockCryptCommands.MarkAsPassword,
                CommandTarget = lbl,
                DataContext = field
            };
            itmPassword.SetBinding(MenuItem.IsCheckedProperty, new Binding("DataContext.Meta") {
                RelativeSource = new RelativeSource(RelativeSourceMode.Self),
                Converter = metaConverter,
                ConverterParameter = FieldMetaData.Password,
                UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
            });
            mnu.Items.Add(itmPassword);
            MenuItem itmPhone = new MenuItem() {
                Header = I18nUtils.GetString("Strings", "MarkAsPhoneNumber"),
                Command = LockCryptCommands.MarkAsPhone,
                CommandTarget = lbl,
                DataContext = field,
            };
            itmPhone.SetBinding(MenuItem.IsCheckedProperty, new Binding("DataContext.Meta") {
                RelativeSource = new RelativeSource(RelativeSourceMode.Self),
                Converter = metaConverter,
                ConverterParameter = FieldMetaData.Phone,
            });
            mnu.Items.Add(itmPhone);
            MenuButton btnMenu = new MenuButton() {
                Content = new Image() { Source = _imgSettings },
                Width = 20,
                Height = 20,
                ToolTip = I18nUtils.GetString("Strings", "Options"),
                MenuLocation = PlacementMode.Right,
                Menu = mnu,
                ShowArrow = false,
                AlwaysShowBorder = true
            };

            //label positioning
            lbl.SetBinding(Grid.RowProperty, new Binding("DataContext.SortIndex") {
                RelativeSource = new RelativeSource(RelativeSourceMode.Self),
                UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
            });
            Grid.SetColumn(lbl, 0);
            grdFields.Children.Add(lbl);

            //password analyzer and 'generate password' button
            PasswordAnalyser pwAnalyzer = new PasswordAnalyser() {
                Padding = new Thickness(0,2,0,2)
            };
            MultiBinding pwGenValueBinding = new MultiBinding() { // hide the analyzer row if a field is not a password
                Converter = isMetaFieldConverter,
                ConverterParameter = FieldMetaData.Password
            };
            pwGenValueBinding.Bindings.Add(new Binding("DataContext.Meta") {
                Source = lbl,
                Mode = BindingMode.TwoWay,
                UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
            });
            pwGenValueBinding.Bindings.Add(new Binding("DataContext.Value") {
                Source = lbl,
                Mode = BindingMode.TwoWay,
                UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
            });
            pwAnalyzer.SetBinding(PasswordAnalyser.PasswordProperty, pwGenValueBinding);

            pwAnalyzer.SetBinding(UIElement.VisibilityProperty, new Binding("DataContext.Meta") {
                Source = lbl,
                Converter = metaVisibilityConverter,
                ConverterParameter = FieldMetaData.Password,
            });
            Button btnPwgen = new Button() {
                Content = new Image() { Source = _imgKey, Width = 16, Height = 16 },
                Margin = new Thickness(0),
                Padding = new Thickness(0),
                ToolTip = I18nUtils.GetString("Strings", "PasswordGenerator"),
                Command = LockCryptCommands.GeneratePassword,
                CommandTarget = lbl
            };
            btnPwgen.SetBinding(UIElement.VisibilityProperty, new Binding("DataContext.Meta") {
                Source = lbl,
                Converter = metaVisibilityConverter,
                ConverterParameter = FieldMetaData.Password,
            });

            // add everything to a container grid with two rows and two columns
            Grid container = new Grid();
            container.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(0, GridUnitType.Auto) });
            container.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(0, GridUnitType.Auto) });
            container.ColumnDefinitions.Add(new ColumnDefinition() { Width= new GridLength(1, GridUnitType.Star)});
            container.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(0, GridUnitType.Auto) });
            Grid.SetColumn(container, 1);
            container.SetBinding(Grid.RowProperty, new Binding("DataContext.SortIndex") {
                Source = lbl,
                UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
            });
            grdFields.Children.Add(container);

            Grid.SetRow(txt, 0);
            Grid.SetColumn(txt, 0);
            container.Children.Add(txt);
            Grid.SetRow(btnMenu, 0);
            Grid.SetColumn(btnMenu, 1);
            container.Children.Add(btnMenu);
            Grid.SetRow(pwAnalyzer, 1);
            Grid.SetColumn(pwAnalyzer, 0);
            container.Children.Add(pwAnalyzer);
            Grid.SetColumn(btnPwgen, 1);
            Grid.SetRow(btnPwgen, 1);
            container.Children.Add(btnPwgen);
            _controlMap.Add(field.Name, new List<UIElement>() { lbl, txt, btnMenu });
        }

        /// <summary>
        /// Initializes a new <see cref="FieldEditor"/> and loads the menu button image.
        /// </summary>
        public FieldEditor() {
            InitializeComponent();
        }

        private void UserControl_Loaded(object sender, RoutedEventArgs e) {
            _parent = Window.GetWindow(this);
            
        }

        /// <summary>
        /// Gets the fields in their edited state.
        /// </summary>
        /// <returns>The fields</returns>
        public IEnumerable<Field> GetFields() {
            List<Field> fields = new List<Field>();
            foreach(UIElement control in grdFields.Children) {
                if(control is Label) {
                    Field boundField = (control as Label).DataContext as Field;
                    fields.Add(boundField);
                }
            }
            return fields;
        }

        /// <summary>
        /// Validates no duplicate fields exist.
        /// </summary>
        /// <returns><c>true</c> if the fields are valid, otherwise <c>false</c>.</returns>
        public bool ValidateFields() {
            bool valid = true;
            Dictionary<string, int> nameCount = new Dictionary<string, int>();
            IEnumerable<Field> fields = GetFields();
            foreach(IField field in fields) {
                if(nameCount.ContainsKey(field.Name)) {
                    nameCount[field.Name] = nameCount[field.Name] + 1;
                } else {
                    nameCount[field.Name] = 1;
                }
            }
            IEnumerable<string> duplicates = nameCount.Where(c => c.Value > 1).Select(c => c.Key);
            if(duplicates.Any()) {
                DialogBox.ShowAlert(Window.GetWindow(this), I18nUtils.GetString("Errors", "SomeFieldsDuplicated") + ": " + duplicates.Aggregate((names, name) => names + ", " + name), I18nUtils.GetString("Errors", "Error"));
                valid = false;
            }
            return valid;
        }

        /// <summary>
        /// Determines whether a field can be moved by the specified sender.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Windows.Input.CanExecuteRoutedEventArgs"/> instance containing the event data.</param>
        private void CanMoveField(object sender, CanExecuteRoutedEventArgs e) {
            if(!IsLoaded)
                return;
            Label lbl = e.OriginalSource as Label;
            if(lbl == null)
                return;
            Field field = lbl.DataContext as Field;
            int direction = int.Parse((e.Parameter ?? "0").ToString());
            if(direction > 0) { // move down
                e.CanExecute = field.SortIndex < Fields.Count() - 1;
            } else if(direction < 0) {
                e.CanExecute = field.SortIndex > 0;
            }
        }

        /// <summary>
        /// Moves a field up or down.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Windows.Input.ExecutedRoutedEventArgs"/> instance containing the event data.</param>
        private void MoveField(object sender, ExecutedRoutedEventArgs e) {
            Label lbl = e.OriginalSource as Label;
            if(lbl == null)
                return;
            Field field = lbl.DataContext as Field;
            int direction = int.Parse((e.Parameter ?? "0").ToString());
            LockCryptBLL.MoveField(field, Fields.Cast<IField>(), direction);
        }

        /// <summary>
        /// Adds a new field.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Windows.Input.ExecutedRoutedEventArgs"/> instance containing the event data.</param>
        private void AddField(object sender, ExecutedRoutedEventArgs e) {
            WinInputDialog dlg = new WinInputDialog(I18nUtils.GetString("Strings", "AddField"), I18nUtils.GetString("Strings", "FieldName")) {
                Owner = _parent,
            };
            dlg.ValidateInput += () => {
                bool valid = true;
                string fieldName = dlg.UserInput.Trim();
                if(fieldName.Length == 0) {
                    DialogBox.ShowAlert(this.Parent as Window, I18nUtils.GetString("Errors", "FieldNameInvalid"), I18nUtils.GetString("Errors", "Error"));
                    valid = false;
                } else {
                    IEnumerable<Field> currentFields = GetFields();
                    Field matchingFieldName = currentFields.FirstOrDefault(f => f.Name.Equals(fieldName, StringComparison.InvariantCultureIgnoreCase));
                    if(matchingFieldName != null) { // field with the same name exists
                        DialogBox.ShowAlert(this.Parent as Window, I18nUtils.GetString("Errors", "FieldExists"), I18nUtils.GetString("Errors", "FieldNameInvalid"));
                        valid = false;
                    }
                }
                return valid;
            };
            if(dlg.ShowDialog() == true) {
                var newField = new Field() {
                    Name = dlg.UserInput.Trim(),
                     SortIndex = Fields.Count
                };
                Fields.Add(newField);
                AddRow(newField);
            }
        }

        /// <summary>
        /// Shows a dialog to rename a field.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Windows.Input.ExecutedRoutedEventArgs"/> instance containing the event data.</param>
        private void RenameField(object sender, ExecutedRoutedEventArgs e) {
            FrameworkElement lbl = e.OriginalSource as FrameworkElement;
            if(lbl == null)
                return;
            Field field = lbl.DataContext as Field;
            WinInputDialog dlg = new WinInputDialog(I18nUtils.GetString("Strings", "RenameField"), I18nUtils.GetString("Strings", "FieldName")) { 
                Owner = _parent,
                UserInput = field.Name
            };
            dlg.ValidateInput += () => {
                bool valid = true;
                string fieldName = dlg.UserInput.Trim();
                if(fieldName.Length == 0) {
                    DialogBox.ShowAlert(this.Parent as Window, I18nUtils.GetString("Errors", "FieldNameInvalid"), I18nUtils.GetString("Errors", "Error"));
                    valid = false;
                } else {
                    IEnumerable<Field> currentFields = GetFields();
                    Field matchingFieldName = currentFields.FirstOrDefault(f => f.Name.Equals(fieldName, StringComparison.InvariantCultureIgnoreCase));
                    if(matchingFieldName != null && matchingFieldName != field) { 
                        // field with the same name exists, and it's not this field.
                        DialogBox.ShowAlert(this.Parent as Window, I18nUtils.GetString("Errors", "FieldExists"), I18nUtils.GetString("Errors", "FieldNameInvalid"));
                        valid = false;
                    }
                }
                return valid;
            };
            var fieldControls = _controlMap[field.Name];

            // update the control map with the new fields name
            _controlMap.Remove(field.Name);
            if(dlg.ShowDialog() == true) {
                field.Name = dlg.UserInput.Trim();
            }
            _controlMap.Add(field.Name, fieldControls);
        }

        /// <summary>
        /// Removes a field.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Windows.Input.ExecutedRoutedEventArgs"/> instance containing the event data.</param>
        private void DeleteField(object sender, ExecutedRoutedEventArgs e) {
            FrameworkElement lbl = e.OriginalSource as FrameworkElement;
            if(lbl == null)
                return;
            Field field = lbl.DataContext as Field;
            IEnumerable<Field> affectedFields = Fields.Where(f => f.SortIndex > field.SortIndex);
            foreach(Field affectedField in affectedFields) {
                affectedField.SortIndex--;
            }

            Fields.Remove(field);
            var fieldControls = _controlMap[field.Name];
            foreach(UIElement control in fieldControls) {
                grdFields.Children.Remove(control);
            }
            _controlMap.Remove(field.Name);
        }

        /// <summary>
        /// Marks a field as a URL.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Windows.Input.ExecutedRoutedEventArgs"/> instance containing the event data.</param>
        private void MarkAsUrl(object sender, ExecutedRoutedEventArgs e) {
            FrameworkElement lbl = e.OriginalSource as FrameworkElement;
            if(lbl == null)
                return;
            Field field = lbl.DataContext as Field;
            LockCryptBLL.ToggleMetaFlag(field, FieldMetaData.Url);
        }

        /// <summary>
        /// Marks a field as a password.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Windows.Input.ExecutedRoutedEventArgs"/> instance containing the event data.</param>
        private void MarkAsPassword(object sender, ExecutedRoutedEventArgs e) {
            FrameworkElement lbl = e.OriginalSource as FrameworkElement;
            if(lbl == null)
                return;
            Field field = lbl.DataContext as Field;
            LockCryptBLL.ToggleMetaFlag(field, FieldMetaData.Password);
        }

        /// <summary>
        /// Marks a field as a phone number.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Windows.Input.ExecutedRoutedEventArgs"/> instance containing the event data.</param>
        private void MarkAsPhone(object sender, ExecutedRoutedEventArgs e) {
            FrameworkElement lbl = e.OriginalSource as FrameworkElement;
            if(lbl == null)
                return;
            Field field = lbl.DataContext as Field;
            LockCryptBLL.ToggleMetaFlag(field, FieldMetaData.Phone);
        }

        /// <summary>
        /// Shows the password generator, then fills a password for a field.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Windows.Input.ExecutedRoutedEventArgs"/> instance containing the event data.</param>
        private void GeneratePassword(object sender, ExecutedRoutedEventArgs e) {
            WinPasswordGenerator dlgPwgen = new WinPasswordGenerator() {
                Owner = _parent,
                IsEditorChild = true
            };
            FrameworkElement lbl = e.OriginalSource as FrameworkElement;
            if(lbl == null)
                return;
            Field field = lbl.DataContext as Field;
            if(dlgPwgen.ShowDialog() == true) {
                field.Value = dlgPwgen.SelectedPassword;
            }
        }
    }
}
